接下來可以要開始撰寫 API 了,可以依照先前設計的 API 規格來實作,如果有特別需求再做調整。
售票系統一定會有使用者登入的機制,無論是系統自行管理或是使用 SSO 進行註冊及登入,實作的窮小子售票系統的重點並不在這些登入及身分驗證的機制,只是要練習高併發的應用,根據之前設計的 API 規格來做開發。
用戶主要交互的服務會是 SalesService 我們在 SalesService 來建立註冊跟登入的 API
API 認證的部分預計使用 JWT 來做驗證,密碼的部分我們使用 Bcrypt 進行加密,首先安裝需要用到的套件
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package BCrypt.Net-Next
新增一個 Token Service 用來產生 Token
/Service/TokenService.cs
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace iThome2024.SalesService.Service;
public class TokenService(IConfiguration configuration)
{
private IConfiguration _configuration = configuration;
public string GenerateJwtToken(string username)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, username),
};
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
claims: claims,
expires: DateTime.Now.AddMinutes(15),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
在 appsettings.json 新增 JWT 需要的參數
"Jwt": {
"Issuer": "iThome2024",
"Key": ""
}
在 Program.cs
註冊服務
builder.Services.AddSingleton<TokenService>();
驗證的部分透過 .NET 框架提供的 Service 進行配置
Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
builder.Services.AddAuthorization();
\\...
var app = builder.Build();
\\...
app.UseAuthentication();
app.UseAuthorization();
為了接收 Body 傳入的帳號密碼,先建立一個 ViewModel
ViewModel/UserSignInViewModel.cs
namespace iThome2024.SalesService.ViewModel;
public class UserSignInViewModel
{
public required string Username { get; set; }
public required string Password { get; set; }
}
新增一個用於註冊的 Endpoint,使用 BCrypt 套件產生 Hash 後的密碼並存入 DB
using BC = BCrypt.Net.BCrypt;
app.MapPost("/api/auth/user", async (UserModel model, [FromServices] TicketSalesContext context) =>
{
model.Username = model.Username.ToUpper();
var user = await context.User.FirstOrDefaultAsync(u => u.Username == model.Username);
if (user != null)
{
return Results.BadRequest("Username already exists");
}
user = new User
{
Username = model.Username,
Password = BC.HashPassword(model.Password)
};
await context.User.AddAsync(user);
await context.SaveChangesAsync();
return Results.Ok("User registered successfully");
});
新增一個用於登入的 Endpoint,接收使用者的帳號密碼,驗證過後透過 TokenService 建立JWT Token 並回傳給使用者
app.MapPost("/api/auth", async (
UserSignInViewModel model,
[FromServices] TicketSalesContext context,
[FromServices] TokenService tokenService) =>
{
model.Username = model.Username.ToUpper();
var user = await context.User.FirstOrDefaultAsync(u => u.Username == model.Username);
if (user == null)
{
return Results.Unauthorized();
}
if (!BC.Verify(model.Password, user.Password))
{
return Results.Unauthorized();
}
var token = tokenService.GenerateJwtToken(model.Username);
return Results.Ok(new { token });
});
目前我們的所有 API 都不需要驗證就可以訪問了,可以拿其中一個 API 加入驗證來做測試,我們拿用來測試 PubSub 的 API 做測試,在 Endpoint 最後加上RequireAuthorization
app.MapPost("/Test/PubSubPublishMessage", async (string message, [FromServices] PublisherService publisherService) =>
{
return await publisherService.Publish(message);
})
.WithName("TestPubSubPublishMessage")
.WithOpenApi()
.RequireAuthorization();
開啟 Postman 測試看看,可以看到現在直接打這個API 會回復我們 401 Unauthorized
要進行測試我們要先註冊一個帳號進行登入再打一次 /Test/PubSubPublishMessage
進到 DB 可以看到使用者已經順利被建立,密碼也是雜湊過的
接著測試看看登入,先測試輸入錯誤的密碼,確實無法順利登入
輸入正確的密碼後就會順利 Response Token 給我們
我們把 Token 填入 Header 後再測試看看 PubSubTest
可以看到驗證通過了 API 也順利返回 MessageId
最後再推送到 Github 上跑 CI/CD 部屬到 Cloud Run ,至此基本的註冊、登入及身分驗證就完成了。
備註: 記得部屬到 Cloud Run 時 appsettings 裡有關 JWT 的設定也要存放到 Secreat Manager